/**
 ** This file is part of FK Klistret Extensions. FK Klistret Extensions 
 ** is free software: you can redistribute it and/or modify it under the 
 ** terms of the GNU General Public License as published by the Free 
 ** Software Foundation, either version 3 of the License, or (at your option) 
 ** any later version.

 ** FK Klistret Extensions is distributed in the hope that it will be 
 ** useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 ** General Public License for more details. You should have received a
 ** copy of the GNU General Public License along with FK Klistret Extensions. 
 ** If not, see <http://www.gnu.org/licenses/>
 */
package se.fk.klistret.extension.crud;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.Message;

import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;

import java.net.URL;

import com.klistret.cmdb.ci.commons.DestinationReference;
import com.klistret.cmdb.ci.commons.Property;
import com.klistret.cmdb.ci.element.process.change.SoftwareInstallation;
import com.klistret.cmdb.ci.element.component.Publication;
import com.klistret.cmdb.ci.element.system.Application;
import com.klistret.cmdb.ci.relation.Composition;

import com.klistret.cmdb.ci.pojo.Element;
import com.klistret.cmdb.ci.pojo.ElementType;
import com.klistret.cmdb.ci.pojo.Relation;
import com.klistret.cmdb.ci.pojo.RelationType;

import com.klistret.cmdb.service.ElementService;
import com.klistret.cmdb.service.ElementTypeService;
import com.klistret.cmdb.service.RelationService;
import com.klistret.cmdb.service.RelationTypeService;

/**
 * Extension reacts to CRUD updates and creates on installations.
 * 
 * 
 * @author Matthew Young
 * 
 */
public class SoftwareInstallationDispatcher {

	/**
	 * SLF4J logger
	 */
	private static final Logger LOGGER = LoggerFactory
			.getLogger(SoftwareInstallationDispatcher.class);

	/**
	 * Software lifecycle URL (dependency injection)
	 */
	protected URL url;

	/**
	 * Relation service (dependency injection)
	 */
	private RelationService relationService;

	/**
	 * Relation type service (dependency injection)
	 */
	private RelationTypeService relationTypeService;

	/**
	 * Element service (dependency injection)
	 */
	private ElementService elementService;

	/**
	 * Element type service (dependency injection)
	 */
	private ElementTypeService elementTypeService;

	/**
	 * Element type denoting an Installation CI (dependency injection)
	 */
	private String elementType;

	/**
	 * Application type
	 */
	private ElementType applicationType;

	/**
	 * Composition type
	 */
	protected RelationType compositionType;

	/**
	 * Software Installation type
	 */
	protected ElementType softwareInstallationType;

	/**
	 * Set relation service
	 * 
	 * @param relationService
	 */
	public void setRelationService(RelationService relationService) {
		this.relationService = relationService;
	}

	public RelationService getRelationService() {
		return this.relationService;
	}

	/**
	 * Set relation type service
	 * 
	 * @param relationTypeService
	 */
	public void setRelationTypeService(RelationTypeService relationTypeService) {
		this.relationTypeService = relationTypeService;
	}

	public RelationTypeService getRelationTypeService() {
		return this.relationTypeService;
	}

	/**
	 * Set element service
	 * 
	 * @param elementService
	 */
	public void setElementService(ElementService elementService) {
		this.elementService = elementService;
	}

	public ElementService getElementService() {
		return this.elementService;
	}

	/**
	 * Set element type service
	 * 
	 * @param elementTypeService
	 */
	public void setElementTypeService(ElementTypeService elementTypeService) {
		this.elementTypeService = elementTypeService;
	}

	public ElementTypeService getElementTypeService() {
		return this.elementTypeService;
	}

	/**
	 * Set element type
	 * 
	 * @param elementType
	 */
	public void setElementType(String elementType) {
		this.elementType = elementType;
	}

	public String getElementType() {
		return this.elementType;
	}

	/**
	 * Is element persistent
	 * 
	 * @param element
	 * @return
	 */
	private boolean isPersistent(Element element) {
		if (element.getId() == null) {
			LOGGER.debug(
					"Element [name: {}, type: {}, version: {}] not persistent",
					new Object[] { element.getName(),
							element.getType().getName(), element.getVersion() });
			return false;
		}
		return true;
	}

	/**
	 * Is element active
	 * 
	 * @param element
	 * @return
	 */
	private boolean isActive(Element element) {
		if (element.getToTimeStamp() != null) {
			LOGGER.debug("Element [id: {}, name: {}, version: {}] not active",
					new Object[] { element.getName(),
							element.getType().getName() });
			return false;
		}
		return true;
	}

	/**
	 * Does the function name match the passed pattern
	 * 
	 * @param functionName
	 * @param pattern
	 * @return
	 */
	private boolean matchesFunction(String functionName, String pattern) {
		if (!functionName.matches(pattern)) {
			LOGGER.debug("Function name {} does not match pattern {}",
					functionName, pattern);
			return false;
		}
		return true;
	}

	/**
	 * Is the element a SoftwareInstallationCI
	 * 
	 * @param element
	 * @return
	 */
	private boolean isSoftwareInstallationCI(Element element) {
		if (!element.getType().getName().equals(elementType)) {
			LOGGER.debug("Element type {} is not {}", element.getType()
					.getName(), elementType);
			return false;
		}
		return true;
	}

	/**
	 * Does the CI originate from Matrix
	 * 
	 * @param element
	 * @return
	 */
	private boolean isMatrix(Element element) {
		if (element.getConfiguration().getOrigin() != null
				&& element.getConfiguration().getOrigin().getName() != null
				&& element.getConfiguration().getOrigin().getName()
						.equals("Matrix")) {
			LOGGER.debug(
					"Element [id: {}, name: {}, version: {}] originates from Matrix",
					new Object[] { element.getId(), element.getName(),
							element.getVersion() });
			return true;
		}
		return false;
	}

	/**
	 * Does the state matched the passed state
	 * 
	 * @param configuration
	 * @param state
	 * @return
	 */
	private boolean matchesState(SoftwareInstallation configuration,
			String state) {
		if (configuration.getState() == null
				|| !configuration.getState().matches(state)) {
			LOGGER.debug("State {} does not equal pattern {}", new Object[] {
					configuration.getState(), state });
			return false;
		}
		return true;
	}

	private boolean targetsSoftware(SoftwareInstallation configuration) {
		if (!configuration
				.getSoftware()
				.getQName()
				.equals("{http://www.klistret.com/cmdb/ci/element/component}Software")) {
			LOGGER.debug("SoftwareInstallation CI does not target Software");
			return false;
		}
		return true;
	}

	/**
	 * Gets application type (checks local cache first)
	 * 
	 * @return ElementType
	 */
	private ElementType getApplicationType() {
		if (applicationType == null)
			applicationType = elementTypeService
					.get("{http://www.klistret.com/cmdb/ci/element/system}Application");

		return applicationType;
	}

	/**
	 * Gets composition type (checks local cache first)
	 * 
	 * @return RelationType
	 */
	protected RelationType getCompositionType() {
		if (compositionType == null)
			compositionType = relationTypeService
					.get("{http://www.klistret.com/cmdb/ci/relation}Composition");

		return compositionType;
	}

	/**
	 * Get Software Installation type
	 * 
	 * @return
	 */
	protected ElementType getSoftwareInstallationType() {
		if (softwareInstallationType == null)
			softwareInstallationType = elementTypeService
					.get("{http://www.klistret.com/cmdb/ci/element/process/change}SoftwareInstallation");

		return softwareInstallationType;
	}

	/**
	 * Update applications with an environment association and the proper
	 * compositions to application software.
	 * 
	 * @param message
	 */
	public void adjustApplication(Message<Element> message) {
		String state = "Completed";

		Element element = message.getPayload();
		String functionName = (String) message.getHeaders().get("function");

		if (!isActive(element) || !isPersistent(element)
				|| !isSoftwareInstallationCI(element) || isMatrix(element)
				|| !matchesFunction(functionName, "UPDATE|CREATE"))
			return;

		SoftwareInstallation configuration = (SoftwareInstallation) element
				.getConfiguration();

		if (!matchesState(configuration, state)
				|| !targetsSoftware(configuration))
			return;

		LOGGER.info(
				"se.fk handler to associate software [id: {}] to an application for installations with state [{}]",
				element.getId(), state);
		try {
			/**
			 * Find all compositions between applications and application
			 * software (these will be removed) where the applications are
			 * earmarked for a particular environment plus the associated
			 * software has the same organization, module, and software type.
			 */
			String environment = configuration.getEnvironment().getName();
			Element software = elementService.get(configuration.getSoftware()
					.getId());

			/**
			 * Find all applications tied to a particular environment with a
			 * name corresponding matching the name of the software then delete.
			 */
			List<String> expressions = Arrays
					.asList(new String[] { String
							.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace element=\"http://www.klistret.com/cmdb/ci/element\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name = \"{http://www.klistret.com/cmdb/ci/element/system}Application\"][pojo:name = \"%s\"]/pojo:configuration[element:Environment = (\"%s\")]",
									software.getName(), environment) });
			Integer count = elementService.count(expressions);

			LOGGER.debug(
					"Found {} applications by name [{}] and associated to environment [{}]",
					new Object[] { count, software.getName(), environment });
			if (count > 100)
				LOGGER.warn("Over 100 will not be deleted.");

			/**
			 * Delete applications
			 */
			List<Element> results = elementService.find(expressions, 0, count);
			for (Element other : results)
				elementService.delete(other.getId());

			/**
			 * Create new application
			 */
			Element application = new Element();
			application.setName(software.getName());
			application.setType(getApplicationType());
			application.setFromTimeStamp(new Date());
			application.setCreateTimeStamp(new Date());

			Application applicationConfiguration = new Application();
			applicationConfiguration.setName(software.getName());
			applicationConfiguration.getEnvironment().add(environment);
			applicationConfiguration.setState("Unknown");

			application.setConfiguration(applicationConfiguration);

			elementService.create(application);

			LOGGER.info(
					"Created an application [id: {}, name: {}] with environment [{}]",
					new Object[] { application.getId(), application.getName(),
							environment });

			/**
			 * Create new composition between the application and the installed
			 * software
			 */
			Composition compositionConfiguration = new Composition();
			compositionConfiguration.setName(String.format("%s,%s",
					application.getName(), software.getName()));

			Relation composition = new Relation();
			composition.setType(getCompositionType());
			composition.setSource(application);
			composition.setDestination(software);
			composition.setCreateTimeStamp(new java.util.Date());
			composition.setFromTimeStamp(new java.util.Date());
			composition.setConfiguration(compositionConfiguration);

			relationService.create(composition);
			LOGGER.info(
					"Created relation [{}] between software [{}] and application [{}]",
					new Object[] { composition.getId(),
							composition.getDestination().getName(),
							composition.getSource().getName() });

		} catch (Exception e) {
			LOGGER.error(
					"Error setting application dependency to environment: {}",
					e.getMessage());
		}
	}

	/**
	 * Get publication dependencies to the Software element
	 * 
	 * @param software
	 * @return
	 */
	private List<Relation> getPublicationDependencies(Element software) {
		List<String> expressions = Arrays
				.asList(new String[] {
						String.format(
								"declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Relation[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/relation}Dependency\"]/pojo:source[pojo:id eq %s]",
								software.getId()),
						"declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Relation/pojo:destination[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/component}Publication\"]" });
		Integer count = relationService.count(expressions);

		if (count > 0)
			return relationService.find(expressions, 0, count);

		return null;
	}

	/**
	 * Construct publication installations based of the original software
	 * installation plus the publication dependencies to the targeted software.
	 * 
	 * @param softwareInstallation
	 * @param dependencies
	 * @return
	 */
	private List<Element> constructPublicationInstallations(
			Element softwareInstallation, List<Relation> dependencies) {
		List<Element> results = new ArrayList<Element>();

		SoftwareInstallation configuration = (SoftwareInstallation) softwareInstallation
				.getConfiguration();

		for (Relation dependency : dependencies) {
			Element publication = dependency.getDestination();

			Element publicationInstallation = new Element();
			publicationInstallation.setType(getSoftwareInstallationType());
			publicationInstallation.setName(publication.getName());
			publicationInstallation.setFromTimeStamp(softwareInstallation
					.getFromTimeStamp());
			publicationInstallation.setCreateTimeStamp(softwareInstallation
					.getCreateTimeStamp());
			publicationInstallation.setUpdateTimeStamp(softwareInstallation
					.getUpdateTimeStamp());

			SoftwareInstallation piConfiguration = new SoftwareInstallation();
			piConfiguration.setEnvironment(configuration.getEnvironment());

			DestinationReference df = new DestinationReference();
			df.setId(publication.getId());
			df.setName(publication.getName());
			df.setQName(publication.getType().getName());
			piConfiguration.setSoftware(df);

			piConfiguration.setVersion(((Publication) publication
					.getConfiguration()).getVersion());
			piConfiguration.setLabel(((Publication) publication
					.getConfiguration()).getLabel());

			piConfiguration.setName(publication.getName());

			piConfiguration.setState(configuration.getState());

			piConfiguration.setTag(configuration.getTag());

			/**
			 * Overload properties as references
			 */
			Property idProperty = new Property();
			idProperty.setName("common.installation.id");
			idProperty.setValue(softwareInstallation.getId().toString());
			piConfiguration.getProperty().add(idProperty);

			if (configuration.getLabel() != null) {
				Property softwareNameProperty = new Property();
				softwareNameProperty.setName("software.label");
				softwareNameProperty.setValue(configuration.getLabel());
				piConfiguration.getProperty().add(softwareNameProperty);
			}

			publicationInstallation.setConfiguration(piConfiguration);
			results.add(publicationInstallation);
		}

		return results;
	}

	/**
	 * Automatically creates publication installations for newly created planned
	 * version installations if the state is Planned. Determines first which
	 * version is associated to an environment and takes the difference in
	 * publications as potential changes if the tags Alla or Samtliga are not
	 * present in the software change.
	 * 
	 * @param message
	 */
	public void createPublicationInstallations(Message<Element> message) {
		String state = "Planned";

		Element element = message.getPayload();
		String functionName = (String) message.getHeaders().get("function");

		if (!isActive(element) || !isPersistent(element)
				|| !isSoftwareInstallationCI(element) || isMatrix(element)
				|| !matchesFunction(functionName, "CREATE"))
			return;

		SoftwareInstallation configuration = (SoftwareInstallation) element
				.getConfiguration();

		if (!matchesState(configuration, state)
				|| !targetsSoftware(configuration))
			return;

		try {
			/**
			 * Get software and all publication dependencies
			 */
			Element software = elementService.get(configuration.getSoftware()
					.getId());
			List<Relation> dependencies = getPublicationDependencies(software);

			/**
			 * Exit if no publication dependencies exist
			 */
			if (dependencies == null || dependencies.size() == 0)
				LOGGER.debug(
						"No dependent publications to software [id: {}, name: {}]",
						software.getId(), software.getName());

			/**
			 * Construct new publication installations
			 */
			if (dependencies.size() > 0) {
				List<Element> candidates = constructPublicationInstallations(
						element, dependencies);

				/**
				 * Find the current software associated with an environment (if
				 * count greater than 2 then do not compare)
				 */
				Element otherSoftware = null;
				List<String> exprs = Arrays
						.asList(new String[] {
								String.format(
										"declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[empty(pojo:toTimeStamp) and pojo:name = (\"%s\") and pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/component}Software\"]",
										software.getName()),
								String.format(
										"declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace element=\"http://www.klistret.com/cmdb/ci/element\"; /pojo:Element/pojo:destinationRelations[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/relation}Composition\"]/pojo:source[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/system}Application\"]/pojo:configuration[element:Environment = (\"%s\")]",
										configuration.getEnvironment()
												.getName()) });
				Integer count = elementService.count(exprs);
				if (count == 1)
					otherSoftware = elementService.unique(exprs);
				if (count > 1)
					LOGGER.warn(
							"{} {} applications associated to the {} environment.",
							new Object[] { count, software.getName(),
									configuration.getEnvironment().getName() });

				/**
				 * Check for flags (Alla, Samtliga)
				 */
				boolean flagged = false;
				for (String tag : element.getConfiguration().getTag())
					if (tag.toLowerCase().matches("alla|samtliga"))
						flagged = true;

				if (flagged)
					LOGGER.info("Software installation flagged to add all publication dependencies as publication installations");

				/**
				 * Remove any similar dependencies
				 */
				if (otherSoftware != null && flagged == false) {
					LOGGER.info(
							"Software [id: {}, name: {}] associated with the environment [name : {}]",
							new Object[] { otherSoftware.getId(),
									otherSoftware.getName(),
									configuration.getEnvironment().getName() });

					List<Relation> otherDependencies = getPublicationDependencies(otherSoftware);

					if (otherDependencies != null) {
						for (Relation otherDependency : otherDependencies) {
							Element otherPublication = otherDependency
									.getDestination();

							Iterator<Element> itr = candidates.iterator();
							while (itr.hasNext()) {
								Element publicationInstallation = itr.next();
								String label = ((SoftwareInstallation) publicationInstallation
										.getConfiguration()).getLabel();

								if (label
										.equals(((Publication) otherPublication
												.getConfiguration()).getLabel())) {
									LOGGER.info(
											"Removing duplicate publication installation [label: {}]",
											label);

									itr.remove();
								}
							}
						}
					}
				}

				/**
				 * Create each publication installation
				 */
				for (Element publicationInstallation : candidates) {
					elementService.create(publicationInstallation);

					SoftwareInstallation piConfiguration = (SoftwareInstallation) publicationInstallation
							.getConfiguration();
					LOGGER.info(
							"Created publication installation [label: {}, id: {}] in reaction to the creation of software installation [label: {}, id: {}]",
							new Object[] { piConfiguration.getLabel(),
									publicationInstallation.getId(),
									configuration.getLabel(), element.getId() });
				}
			}
		} catch (Exception e) {
			LOGGER.error("Error creating publication installations: {}",
					e.getMessage());
		}
	}

	/**
	 * Makes sure that the tags in a SoftwareInstallation CI exist in the
	 * associated publication installations.
	 * 
	 * @param message
	 */
	public void retagPublicationInstallations(Message<Element> message) {
		Element element = message.getPayload();
		String functionName = (String) message.getHeaders().get("function");

		if (!isActive(element) || !isPersistent(element)
				|| !isSoftwareInstallationCI(element) || isMatrix(element)
				|| !matchesFunction(functionName, "UPDATE"))
			return;

		SoftwareInstallation configuration = (SoftwareInstallation) element
				.getConfiguration();

		if (!targetsSoftware(configuration))
			return;

		if (configuration.getTag().size() == 0) {
			LOGGER.debug(
					"Software installation has no tags to recopy [id: {}, name: {}, version: {}]",
					new Object[] { element.getId(), element.getName(),
							element.getVersion() });
			return;
		}

		try {
			List<String> expressions = Arrays
					.asList(new String[] { String
							.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; declare namespace change=\"http://www.klistret.com/cmdb/ci/element/process/change\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/process/change}SoftwareInstallation\"]/pojo:configuration[commons:Property/commons:Name = (\"common.installation.id\") and commons:Property/commons:Value = (\"%d\")]/change:Software[commons:QName eq \"{http://www.klistret.com/cmdb/ci/element/component}Publication\"]",
									element.getId()) });
			Integer count = elementService.count(expressions);

			if (count == 0)
				LOGGER.debug(
						"No dependent publication installations to software installation [id: {}, name: {}, version: {}]",
						new Object[] { element.getId(), element.getName(),
								element.getVersion() });

			if (count > 0) {
				List<Element> results = elementService.find(expressions, 0,
						count);
				for (Element other : results) {
					boolean dirty = false;
					for (String tag : element.getConfiguration().getTag())
						if (!other.getConfiguration().getTag().contains(tag)) {
							dirty = true;
							other.getConfiguration().getTag().add(tag);
						}

					if (dirty) {
						elementService.update(other);
						LOGGER.info(
								"Updated tags on publication installation [id: {}, name: {}, version: {}]",
								new Object[] { other.getId(), other.getName(),
										other.getVersion() });
					}
				}
			}
		} catch (Exception e) {
			LOGGER.error("Error retagging publication installations: {}",
					e.getMessage());
		}
	}

	/**
	 * Delete PublcationInstallationCI that are connected to
	 * SoftwareInstallationCI when they are deleted.
	 * 
	 * @param message
	 */
	public void deletePublicationInstallations(Message<Element> message) {
		Element element = message.getPayload();
		String functionName = (String) message.getHeaders().get("function");

		if (isActive(element) || !isPersistent(element)
				|| !isSoftwareInstallationCI(element) || isMatrix(element)
				|| !matchesFunction(functionName, "DELETE"))
			return;

		SoftwareInstallation configuration = (SoftwareInstallation) element
				.getConfiguration();

		if (!targetsSoftware(configuration))
			return;

		try {
			List<String> expressions = Arrays
					.asList(new String[] { String
							.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; declare namespace change=\"http://www.klistret.com/cmdb/ci/element/process/change\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/process/change}SoftwareInstallation\"]/pojo:configuration[commons:Property/commons:Name = (\"common.installation.id\") and commons:Property/commons:Value = (\"%d\")]/change:Software[commons:QName eq \"{http://www.klistret.com/cmdb/ci/element/component}Publication\"]",
									element.getId()) });
			Integer count = elementService.count(expressions);

			if (count == 0)
				LOGGER.debug(
						"No dependent publication installations to software installation [id: {}, name: {}, version: {}]",
						new Object[] { element.getId(), element.getName(),
								element.getVersion() });

			if (count > 0) {
				List<Element> results = elementService.find(expressions, 0,
						count);
				for (Element other : results) {
					elementService.delete(other.getId());
					LOGGER.info(
							"Deleted publication installation [id: {}, name: {}, version: {}]",
							new Object[] { other.getId(), other.getName(),
									other.getVersion() });

				}
			}
		} catch (Exception e) {
			LOGGER.error("Error deleting publication installations: {}",
					e.getMessage());
		}
	}
}
